"""
Sing-box 进程管理器（改进版）
采用"配置重载"模式，而不是动态API修改
"""
import json
import subprocess
import signal
import time
import os
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from api.utils.logger import get_logger

logger = get_logger("singbox_process_manager")

class SingBoxProcessManager:
    """
    Sing-box进程管理器 - 采用配置重载模式
    
    核心原则：
    1. Python维护所有代理配置（数据库/内存）
    2. 每次变动生成完整的config.json
    3. 通过热重载让Sing-box应用新配置 
    """
    
    # 配置文件路径
    CONFIG_FILE = Path("runtime/singbox/service_config.json")
    PID_FILE = Path("runtime/singbox/singbox.pid")
    
    # Sing-box可执行文件
    _singbox_path = None
    _process = None
    
    # 内存中的配置状态（真理之源）
    _proxy_configs = {}  # {tag: {port, v2ray_config}}
    
    @classmethod
    def initialize(cls):
        """初始化进程管理器"""
        # 查找sing-box可执行文件
        from api.service.singbox_manager import SingBoxManager
        cls._singbox_path = SingBoxManager.get_singbox_path()
        
        if not cls._singbox_path:
            logger.warning("未找到sing-box可执行文件")
            return False
        
        # 确保目录存在
        cls.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
        
        # 强制重启进程以应用新配置（不管是否已在运行）
        logger.info("初始化：准备启动Sing-box进程...")
        success, message = cls._start_process()
        
        if success:
            logger.info(f"✓ Sing-box初始化成功: {message}")
        else:
            logger.error(f"✗ Sing-box初始化失败: {message}")
        
        return success
    
    @classmethod
    def add_proxy(cls, tag: str, port: int, v2ray_config: Dict) -> Tuple[bool, str]:
        """
        添加代理配置
        
        Args:
            tag: 代理标签
            port: SOCKS5端口
            v2ray_config: V2Ray配置字典
            
        Returns:
            (是否成功, 消息)
        """
        try:
            # 检查配置是否已存在
            if tag in cls._proxy_configs:
                existing_port = cls._proxy_configs[tag]['port']
                return True, f"配置已存在（端口 {existing_port}）"
            
            # 添加到内存状态
            cls._proxy_configs[tag] = {
                'port': port,
                'v2ray_config': v2ray_config
            }
            
            logger.info(f"添加新代理配置: {tag}, 端口: {port}")
            
            # 重新生成完整配置并重载
            success, message = cls._regenerate_and_reload()
            
            if success:
                return True, f"代理添加成功（端口 {port}）"
            else:
                # 失败则回滚
                del cls._proxy_configs[tag]
                return False, message
                
        except Exception as e:
            logger.error(f"添加代理失败: {e}", exc_info=True)
            return False, f"添加失败: {str(e)}"
    
    @classmethod
    def remove_proxy(cls, tag: str) -> Tuple[bool, str]:
        """
        移除代理配置
        
        Args:
            tag: 代理标签
            
        Returns:
            (是否成功, 消息)
        """
        try:
            if tag not in cls._proxy_configs:
                return True, "配置不存在"
            
            # 删除配置
            del cls._proxy_configs[tag]
            logger.info(f"移除代理配置: {tag}")
            
            # 重新生成完整配置并重载
            success, message = cls._regenerate_and_reload()
            
            if success:
                return True, "已移除配置"
            else:
                return False, message
            
        except Exception as e:
            logger.error(f"移除代理失败: {e}", exc_info=True)
            return False, f"移除失败: {str(e)}"
    
    @classmethod
    def _regenerate_and_reload(cls) -> Tuple[bool, str]:
        """
        重新生成完整配置并重载Sing-box
        
        这是核心方法：
        1. 根据内存状态生成完整的config.json
        2. 原子写入文件（先写临时文件再重命名）
        3. 通过 Clash API 热重载配置（失败则重启进程）
        """
        try:
            # 生成完整配置
            config = cls._generate_complete_config()
            
            # 原子写入（防止读取时文件损坏）
            temp_file = cls.CONFIG_FILE.with_suffix('.json.tmp')
            with open(temp_file, 'w', encoding='utf-8') as f:
                json.dump(config, f, indent=2, ensure_ascii=False)
            
            # 原子替换
            temp_file.replace(cls.CONFIG_FILE)
            
            logger.info("配置文件已更新")
            
            # 重载Sing-box
            if cls._is_process_running():
                # 先尝试 API 热重载
                if cls._reload_via_api():
                    logger.info("✓ 通过 Clash API 热重载成功")
                    return True, "配置已热重载"
                else:
                    logger.warning("API 热重载失败，正在重启进程...")
                    return cls._restart_process()
            else:
                # 进程不存在，启动它
                return cls._start_process()
                
        except Exception as e:
            logger.error(f"重新生成配置失败: {e}", exc_info=True)
            return False, f"配置生成失败: {str(e)}"
    
    @classmethod
    def _regenerate_config_file_only(cls) -> Tuple[bool, str]:
        """仅重新生成配置文件，不重载或启动进程"""
        try:
            config = cls._generate_complete_config()
            temp_file = cls.CONFIG_FILE.with_suffix('.json.tmp')
            with open(temp_file, 'w', encoding='utf-8') as f:
                json.dump(config, f, indent=2, ensure_ascii=False)
            temp_file.replace(cls.CONFIG_FILE)
            logger.info("配置文件已生成")
            return True, "配置文件已生成"
        except Exception as e:
            logger.error(f"生成配置文件失败: {e}", exc_info=True)
            return False, f"生成配置文件失败: {str(e)}"

    @classmethod
    def _generate_complete_config(cls) -> Dict:
        """
        生成完整的Sing-box配置
        
        包含所有inbounds、outbounds和route规则
        """
        from api.utils.singbox_proxy_helper import SingBoxProxyManager
        
        # 生成日志文件路径（按天分割）
        from datetime import datetime
        log_date = datetime.now().strftime('%Y%m%d')
        log_file = f"runtime/logs/singbox_{log_date}.log"
        
        # 确保日志目录存在
        Path(log_file).parent.mkdir(parents=True, exist_ok=True)
        
        # 确保缓存目录存在
        cache_dir = Path("runtime/singbox/cache")
        cache_dir.mkdir(parents=True, exist_ok=True)
        
        config = {
            "log": {
                "level": "info",
                "timestamp": True,
                "output": log_file
            },
            "dns": {
                "servers": [
                    {
                        "tag": "cloudflare-doh",
                        "address": "https://177cowl5hr.cloudflare-gateway.com/dns-query",
                        "detour": "proxy-dns"
                    },
                    {
                        "tag": "google-doh",
                        "address": "https://dns.google/dns-query",
                        "detour": "proxy-dns"
                    },
                    {
                        "tag": "local",
                        "address": "223.5.5.5",
                        "detour": "direct"
                    }
                ],
                "fakeip": {
                    "enabled": True,
                    "inet4_range": "198.18.0.0/15",
                    "inet6_range": "fc00::/18"
                },
                "strategy": "prefer_ipv4",
                "final": "cloudflare-doh"
            },
            "inbounds": [],
            "outbounds": [],
            "route": {
                "rules": []
            },
            "experimental": {
                "clash_api": {
                    "external_controller": "127.0.0.1:9090",
                    "secret": "",
                    "default_mode": "rule"
                },
                "cache_file": {
                    "enabled": True,
                    "path": "runtime/singbox/cache/cache.db",
                    "store_fakeip": False
                }
            }
        }
        
        # 收集所有已使用的端口
        used_ports = set(info['port'] for info in cls._proxy_configs.values())
        logger.debug(f"已使用端口: {used_ports}")
        
        # 为每个代理生成inbound和outbound
        for tag, proxy_info in cls._proxy_configs.items():
            port = proxy_info['port']
            v2ray_config = proxy_info['v2ray_config']
            
            # 生成inbound（SOCKS5监听，启用流量嗅探）
            inbound_tag = f"in_{tag}"
            inbound = {
                "type": "socks",
                "tag": inbound_tag,
                "listen": "127.0.0.1",
                "listen_port": port,
                "sniff": True,
                "sniff_override_destination": True,
                "users": []
            }
            config['inbounds'].append(inbound)
            
            # 生成outbound（代理节点）
            singbox_config = SingBoxProxyManager.generate_singbox_config(v2ray_config, port)
            outbound = singbox_config['outbounds'][0]
            outbound['tag'] = tag
            config['outbounds'].append(outbound)
            
            # 生成路由规则（inbound -> outbound）
            rule = {
                "inbound": [inbound_tag],
                "outbound": tag
            }
            config['route']['rules'].append(rule)
        
        # 添加默认直连outbound（作为fallback）
        config['outbounds'].append({
            "type": "direct",
            "tag": "direct"
        })
        
        # 添加DNS代理outbound（selector，优先使用第一个代理，回退到direct）
        if cls._proxy_configs:
            first_proxy_tag = next(iter(cls._proxy_configs.keys()))
            config['outbounds'].append({
                "type": "selector",
                "tag": "proxy-dns",
                "outbounds": [first_proxy_tag, "direct"]
            })
        else:
            # 如果没有代理，DNS也走直连
            config['outbounds'].append({
                "type": "direct",
                "tag": "proxy-dns"
            })
        
        return config
    
    @classmethod
    def _is_pid_running(cls, pid: int) -> bool:
        """检查指定PID的进程是否在运行"""
        if pid <= 0:
            return False
        try:
            os.kill(pid, 0)
            return True
        except ProcessLookupError:
            return False
        except Exception:
            return False

    @classmethod
    def _is_process_running(cls) -> bool:
        """检查Sing-box进程是否在运行"""
        if not cls.PID_FILE.exists():
            return False
        
        try:
            with open(cls.PID_FILE, 'r') as f:
                pid = int(f.read().strip())
            
            is_running = cls._is_pid_running(pid)
            if not is_running:
                logger.warning(f"PID文件存在，但进程 (PID: {pid}) 未运行。清理PID文件。")
                cls.PID_FILE.unlink()
            return is_running
            
        except (ValueError, IOError):
            logger.warning("无效的PID文件，正在清理...")
            cls.PID_FILE.unlink()
            return False
        except Exception as e:
            logger.error(f"检查进程失败: {e}")
            return False
    
    @classmethod
    def _start_process(cls) -> Tuple[bool, str]:
        """启动Sing-box进程"""
        try:
            if not cls._singbox_path:
                return False, "Sing-box未安装"
            
            # ========== 启动前强制清理所有可能的旧进程 ==========
            logger.info("=" * 60)
            logger.info("开始清理所有Sing-box进程...")
            
            killed_count = 0
            
            # 方法1: 通过PID文件清理
            if cls.PID_FILE.exists():
                try:
                    with open(cls.PID_FILE, 'r') as f:
                        old_pid = int(f.read().strip())
                    logger.info(f"[方法1] 从PID文件读取到进程: {old_pid}")
                    
                    # 尝试杀死进程组
                    try:
                        pgid = os.getpgid(old_pid)
                        logger.info(f"[方法1] 终止进程组 PGID: {pgid}")
                        os.killpg(pgid, signal.SIGKILL)
                        killed_count += 1
                        time.sleep(0.3)
                    except Exception as e:
                        logger.debug(f"[方法1] 进程组终止失败: {e}")
                    
                    # 直接杀死PID
                    try:
                        os.kill(old_pid, signal.SIGKILL)
                        logger.info(f"[方法1] 已发送SIGKILL到PID: {old_pid}")
                        killed_count += 1
                        time.sleep(0.3)
                    except ProcessLookupError:
                        logger.info(f"[方法1] PID {old_pid} 已不存在")
                    
                    cls.PID_FILE.unlink()
                    logger.info(f"[方法1] 已删除PID文件")
                except Exception as e:
                    logger.warning(f"[方法1] 清理失败: {e}")
            
            # 方法2: pkill -9 按可执行文件路径
            try:
                logger.info(f"[方法2] 使用pkill清理: {cls._singbox_path}")
                result = subprocess.run(
                    ['pkill', '-9', '-f', cls._singbox_path],
                    capture_output=True,
                    text=True,
                    timeout=3
                )
                if result.returncode == 0:
                    logger.info(f"[方法2] pkill成功清理进程")
                    killed_count += 1
                else:
                    logger.info(f"[方法2] pkill未找到进程 (returncode: {result.returncode})")
                time.sleep(0.5)
            except Exception as e:
                logger.debug(f"[方法2] pkill失败: {e}")
            
            # 方法3: pkill -9 按进程名
            try:
                singbox_name = os.path.basename(cls._singbox_path)
                logger.info(f"[方法3] 使用pkill清理进程名: {singbox_name}")
                result = subprocess.run(
                    ['pkill', '-9', singbox_name],
                    capture_output=True,
                    text=True,
                    timeout=3
                )
                if result.returncode == 0:
                    logger.info(f"[方法3] pkill按名称成功清理")
                    killed_count += 1
                time.sleep(0.5)
            except Exception as e:
                logger.debug(f"[方法3] pkill按名称失败: {e}")
            
            # 方法4: killall -9
            try:
                singbox_name = os.path.basename(cls._singbox_path)
                logger.info(f"[方法4] 使用killall清理: {singbox_name}")
                result = subprocess.run(
                    ['killall', '-9', singbox_name],
                    capture_output=True,
                    text=True,
                    timeout=3
                )
                if result.returncode == 0:
                    logger.info(f"[方法4] killall成功清理")
                    killed_count += 1
                time.sleep(0.5)
            except Exception as e:
                logger.debug(f"[方法4] killall失败: {e}")
            
            # 方法5: 通过ps + grep + kill查找并杀死所有相关进程
            try:
                logger.info(f"[方法5] 使用ps+grep查找残留进程")
                result = subprocess.run(
                    ['ps', 'aux'],
                    capture_output=True,
                    text=True,
                    timeout=3
                )
                if result.returncode == 0:
                    for line in result.stdout.splitlines():
                        if 'sing-box' in line.lower() and 'grep' not in line:
                            parts = line.split()
                            if len(parts) > 1:
                                try:
                                    pid = int(parts[1])
                                    logger.info(f"[方法5] 发现残留进程 PID: {pid}, 正在终止...")
                                    os.kill(pid, signal.SIGKILL)
                                    killed_count += 1
                                    time.sleep(0.2)
                                except Exception as e:
                                    logger.debug(f"[方法5] 终止PID {pid}失败: {e}")
            except Exception as e:
                logger.debug(f"[方法5] ps+grep失败: {e}")
            
            # 最终等待
            logger.info(f"清理完成，共执行 {killed_count} 次杀进程操作，等待2秒确保进程完全退出...")
            time.sleep(2)
            
            # 最终检查
            if cls._is_process_running():
                logger.error("⚠️ 警告：清理后仍检测到PID文件中的进程存在！")
                logger.info("强制删除PID文件并继续...")
                if cls.PID_FILE.exists():
                    cls.PID_FILE.unlink()
            else:
                logger.info("✓ 确认：所有旧进程已清理")
            
            logger.info("=" * 60)
            
            # 如果配置文件不存在，生成完整的初始配置
            if not cls.CONFIG_FILE.exists():
                from datetime import datetime
                log_date = datetime.now().strftime('%Y%m%d')
                log_file = f"runtime/logs/singbox_{log_date}.log"
                
                empty_config = {
                    "log": {"level": "info", "timestamp": True, "output": log_file},
                    "dns": {
                        "servers": [
                            {"tag": "google", "address": "8.8.8.8", "detour": "proxy-dns"},
                            {"tag": "local", "address": "223.5.5.5", "detour": "direct"}
                        ],
                        "strategy": "prefer_ipv4",
                        "final": "google"
                    },
                    "inbounds": [],
                    "outbounds": [
                        {"type": "direct", "tag": "direct"},
                        {"type": "direct", "tag": "proxy-dns"}
                    ],
                    "route": {"rules": []}
                }
                with open(cls.CONFIG_FILE, 'w') as f:
                    json.dump(empty_config, f, indent=2)
            
            # 启动进程（日志输出到文件，同时捕获stderr用于错误诊断）
            from datetime import datetime
            log_date = datetime.now().strftime('%Y%m%d')
            log_file = Path(f"runtime/logs/singbox_{log_date}.log")
            log_file.parent.mkdir(parents=True, exist_ok=True)
            
            cmd = [cls._singbox_path, 'run', '-c', str(cls.CONFIG_FILE)]
            logger.info(f"启动命令: {' '.join(cmd)}")
            
            # 使用PIPE捕获stderr以便检测启动错误
            cls._process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                start_new_session=True,
                text=True
            )
            
            # 保存PID
            with open(cls.PID_FILE, 'w') as f:
                f.write(str(cls._process.pid))
            
            # 等待0.5秒检查是否立即崩溃
            time.sleep(0.5)
            
            if cls._process.poll() is not None:
                # 进程已结束，读取错误信息
                stdout_output, stderr_output = cls._process.communicate()
                error_msg = stderr_output if stderr_output else stdout_output
                logger.error(f"Sing-box启动失败，退出码: {cls._process.returncode}")
                logger.error(f"错误输出:\n{error_msg}")
                
                # 删除PID文件
                if cls.PID_FILE.exists():
                    cls.PID_FILE.unlink()
                
                return False, f"启动失败: {error_msg[:200]}"
            else:
                # 进程运行正常，将输出重定向到日志文件
                import threading
                
                def log_output(pipe, log_file):
                    with open(log_file, 'a') as f:
                        for line in pipe:
                            f.write(line)
                            f.flush()
                
                threading.Thread(target=log_output, args=(cls._process.stdout, log_file), daemon=True).start()
                threading.Thread(target=log_output, args=(cls._process.stderr, log_file), daemon=True).start()
                
                logger.info(f"Sing-box进程已启动，PID: {cls._process.pid}")
                return True, "Sing-box已启动"
                
        except Exception as e:
            logger.error(f"启动进程失败: {e}", exc_info=True)
            return False, f"启动失败: {str(e)}"
    
    @classmethod
    def _reload_via_api(cls) -> bool:
        """
        通过 Clash API 热重载配置
        
        使用 Sing-box 的 Clash 兼容 API 进行配置热重载
        force=false 表示尽量保留旧连接，新连接才应用新规则
        """
        try:
            import requests
            
            # Clash API 端点
            api_url = "http://127.0.0.1:9090/configs?force=false"
            
            # 必须使用绝对路径
            config_path = str(cls.CONFIG_FILE.absolute())
            
            payload = {"path": config_path}
            
            logger.info(f"正在通过 Clash API 热重载配置: {config_path}")
            
            response = requests.put(api_url, json=payload, timeout=5)
            
            if response.status_code == 204:
                logger.info("✓ Clash API 返回成功 (204 No Content)")
                return True
            else:
                logger.error(f"✗ Clash API 返回错误: {response.status_code} - {response.text}")
                return False
                
        except requests.exceptions.ConnectionError:
            logger.error("✗ 无法连接到 Clash API (127.0.0.1:9090)，可能 API 未启用")
            return False
        except requests.exceptions.Timeout:
            logger.error("✗ Clash API 请求超时")
            return False
        except Exception as e:
            logger.error(f"✗ Clash API 调用异常: {e}", exc_info=True)
            return False
    
    @classmethod
    def _reload_process(cls) -> Tuple[bool, str]:
        """
        重载Sing-box配置（已弃用，改用 _reload_via_api）
        
        发送HUP信号让进程重新读取配置文件
        """
        try:
            if not cls.PID_FILE.exists():
                return False, "PID文件不存在"
            
            with open(cls.PID_FILE, 'r') as f:
                pid = int(f.read().strip())
            
            # 发送HUP信号
            os.kill(pid, signal.SIGHUP)
            
            logger.info(f"已向Sing-box (PID: {pid}) 发送重载信号")
            return True, "配置已重载"
            
        except ProcessLookupError:
            logger.error("Sing-box进程不存在")
            # 进程死了，重新启动
            return cls._start_process()
        except Exception as e:
            logger.error(f"重载失败: {e}", exc_info=True)
            return False, f"重载失败: {str(e)}"
    
    @classmethod
    def _restart_process(cls) -> Tuple[bool, str]:
        """
        重启 Sing-box 进程
        """
        try:
            logger.info("正在重启 Sing-box 进程...")
            
            # 先停止
            stop_success, stop_msg = cls.stop_process()
            if not stop_success:
                logger.warning(f"停止进程时出现问题: {stop_msg}")
            
            # 等待一下确保端口释放
            time.sleep(1)
            
            # 再启动
            return cls._start_process()
            
        except Exception as e:
            logger.error(f"重启进程失败: {e}", exc_info=True)
            return False, f"重启失败: {str(e)}"
    
    @classmethod
    def stop_process(cls) -> Tuple[bool, str]:
        """停止Sing-box进程，增加鲁棒性"""
        if not cls.PID_FILE.exists():
            logger.info("PID文件不存在，无需停止")
            return True, "进程未运行"
        
        try:
            with open(cls.PID_FILE, 'r') as f:
                pid = int(f.read().strip())
        except (ValueError, IOError) as e:
            logger.error(f"读取PID文件失败: {e}")
            # 如果PID文件无效，直接删除
            cls.PID_FILE.unlink()
            return True, "无效的PID文件，已删除"

        try:
            # 获取进程组ID (对于由 start_new_session=True 启动的进程，PGID通常等于PID)
            pgid = os.getpgid(pid)
            logger.info(f"准备停止Sing-box进程组 (PID: {pid}, PGID: {pgid})...")

            # 1. 发送SIGTERM信号到整个进程组
            logger.info(f"正在向Sing-box进程组 (PGID: {pgid}) 发送SIGTERM信号...")
            os.killpg(pgid, signal.SIGTERM)

            # 2. 等待进程退出
            for _ in range(50):
                time.sleep(0.1)
                if not cls._is_pid_running(pid):
                    logger.info(f"Sing-box (PID: {pid}) 已成功停止")
                    cls.PID_FILE.unlink()
                    return True, "进程已成功停止"
            
            # 3. 如果仍在运行，发送SIGKILL到整个进程组
            logger.warning(f"Sing-box (PGID: {pgid}) 未能优雅关闭，发送SIGKILL...")
            os.killpg(pgid, signal.SIGKILL)
            
            # 在发送SIGKILL后，我们假定OS会完成终止操作，即使有延迟。
            # 不再检查，直接清理并返回成功。
            logger.info(f"已向Sing-box (PGID: {pgid}) 发送强制终止信号，假定进程将被终止。")
            cls.PID_FILE.unlink()
            return True, "已发送强制停止信号"

        except ProcessLookupError:
            logger.info(f"Sing-box进程 (PID: {pid}) 已不存在")
            cls.PID_FILE.unlink()
            return True, "进程已不存在"
        except Exception as e:
            logger.error(f"停止进程时发生未知错误: {e}", exc_info=True)
            return False, f"停止失败: {str(e)}"
    
    @classmethod
    def get_proxy_list(cls) -> List[Dict]:
        """获取当前所有代理配置"""
        return [
            {
                'tag': tag,
                'port': info['port']
            }
            for tag, info in cls._proxy_configs.items()
        ]
    
    @classmethod
    def clear_all_proxies(cls):
        """清除所有代理配置"""
        cls._proxy_configs.clear()
        cls._regenerate_and_reload()
        logger.info("已清除所有代理配置")